#include <exception>
#include "Renderer.h"
#include <pybind11/stl.h> // Cannot leave out
#include <pybind11/pybind11.h>

PYBIND11_MODULE(nitro, m)
{
    namespace py = pybind11;
    using namespace Nitro::Renderer;

    auto checkOpenGLError = [] {
        switch (glGetError())
        {
            case GL_NO_ERROR: break;
            case GL_INVALID_ENUM:
                throw std::runtime_error("GL_INVALID_ENUM");
                break;
            case GL_INVALID_VALUE:
                throw std::runtime_error("GL_INVALID_VALUE");
                break;
            case GL_INVALID_INDEX:
                throw std::runtime_error("GL_INVALID_INDEX");
                break;
            case GL_INVALID_OPERATION:
                throw std::runtime_error("GL_INVALID_OPERATION");
                break;
            case GL_STACK_OVERFLOW:
                throw std::runtime_error("GL_STACK_OVERFLOW");
                break;
            case GL_STACK_UNDERFLOW:
                throw std::runtime_error("GL_STACK_UNDERFLOW");
                break;
            case GL_OUT_OF_MEMORY:
                throw std::runtime_error("GL_OUT_OF_MEMORY");
                break;
            default: throw std::runtime_error("Unknown exception");
        };
    };

    class Utils
    {
    };

    m.doc() = "A simple OpenGL abstraction layer";

    m.def("setVertSync", &glfwSwapInterval, py::arg("interval"),
          py::doc("Set vertical synchronization value (0, 1, -1)"));
    m.def(
        "deltaTime",
        []() {
            static double prevTime = 0.0, currentTime = 0.0;
            currentTime = glfwGetTime();
            auto delta = currentTime - prevTime;
            prevTime = currentTime;
            return delta * 1000.0;
        },
        py::doc("Get delta time (in milliseconds) since last called"));
    m.def(
        "extensionSupported",
        [](const char* name) -> bool { return glfwExtensionSupported(name); },
        py::arg("extension"));

    py::class_<Window>(m, "Window")
        .def(py::init<const char*, int, int>(), py::arg("title") = "nitro",
             py::arg("width") = 800, py::arg("height") = 600)
        .def("title", &Window::title, py::doc("Get window title"))
        .def("setTitle", &Window::setTitle, py::arg("title"),
             py::doc("Set window title"))
        .def("setSize", &Window::setSize, py::arg("width"), py::arg("height"),
             py::doc("Set window size"))
        .def("running", &Window::running,
             py::doc("Check if the window is running"))
        .def("update", &Window::update,
             py::doc("This function is called per frame to swap buffers and "
                     "poll events"));

    py::class_<Buffer>(m, "Buffer")
        .def(py::init([] {
            Buffer buffer{0};
            return buffer;
        }))
        .def_readonly("id", &Buffer::id,
                      py::doc("Return value for glCreateBuffer()"))
        .def_readonly("vao", &Buffer::vao,
                      py::doc("ID for VertexArray Object (VAO)"))
        .def_readonly("vbo", &Buffer::vbo,
                      py::doc("ID for VertexBuffer Object (VBO)"));

    m.def("ClearColor", &ClearColor, py::arg("r") = 0.f, py::arg("g") = 0.f,
          py::arg("b") = 0.f, py::arg("a") = 1.f,
          py::doc("Clear color buffer"));
    m.def(
        "Clear", [] { Clear(); }, py::doc("Clear depth and color buffer"));

    py::enum_<ShaderType>(m, "ShaderType")
        .value("VertexShader", ShaderType::VertexShader)
        .value("FragmentShader", ShaderType::FragmentShader)
        .value("ComputeShader", ShaderType::ComputeShader)
        .value("GeometryShader", ShaderType::GeometryShader);
    py::enum_<ShaderSourceType>(m, "ShaderSourceType")
        .value("SourceCode", ShaderSourceType::SourceCode)
        .value("SourceFile", ShaderSourceType::SourceFile);
    py::class_<ShaderDescriptor>(m, "ShaderDescriptor")
        .def(py::init())
        .def_readwrite("type", &ShaderDescriptor::type)
        .def_readwrite("source", &ShaderDescriptor::source)
        .def_readwrite("sourceType", &ShaderDescriptor::sourceType);
    m.def(
        "CreateShader",
        [](const ShaderDescriptor& desc) {
            auto shader = CreateShader(desc);
            if (shader.hasReport())
            {
                throw std::runtime_error(shader.getReport());
            }
            return shader.id;
        },
        py::arg("shaderDescriptor"));

    py::enum_<BindFlags>(m, "BindFlags")
        .value("VertexBuffer", BindFlags::VertexBuffer)
        .value("IndexBuffer", BindFlags::IndexBuffer);
    py::enum_<Format>(m, "Format")
        .value("R32Float", Format::R32Float)
        .value("RG32Float", Format::RG32Float)
        .value("RGB32Float", Format::RGB32Float)
        .value("RGB8UInt", Format::RGB8UInt)
        .value("RGBA8UInt", Format::RGBA8UInt);
    py::class_<BufferDescriptor>(m, "BufferDescriptor")
        .def(py::init())
        .def_readwrite("bindFlags", &BufferDescriptor::bindFlags)
        .def_readwrite("size", &BufferDescriptor::size)
        .def(
            "appendAttribute",
            static_cast<void (BufferDescriptor::*)(uint32_t, uint32_t, Format)>(
                &BufferDescriptor::appendAttribute),
            py::arg("offset"), py::arg("stride"), py::arg("format"));
    m.def(
        "CreateBuffer",
        [=](const BufferDescriptor& desc, const py::object& data) {
            if (desc.bindFlags == VertexBuffer)
            {
                return CreateBuffer(desc,
                                    data.cast<std::vector<float>>().data());
            }
            else if (desc.bindFlags == IndexBuffer)
            {
                return CreateBuffer(desc,
                                    data.cast<std::vector<uint32_t>>().data());
            }
            checkOpenGLError();
            return Buffer{0ull};
        },
        py::arg("bufferDescriptor"), py::arg("data"));

    py::enum_<TextureFilter>(m, "TextureFilter")
        .value("Linear", TextureFilter::Linear)
        .value("Nearest", TextureFilter::Nearest)
        .value("LinearMipmapLinear", TextureFilter::LinearMipmapLinear)
        .value("LinearMipmapNearest", TextureFilter::LinearMipmapNearest)
        .value("NearestMipmapLinear", TextureFilter::NearestMipmapLinear)
        .value("NearestMipmapNearest", TextureFilter::NearestMipmapNearest);
    py::enum_<TextureWrapping>(m, "TextureWrapping")
        .value("Repeat", TextureWrapping::Repeat)
        .value("MirroredRepeat", TextureWrapping::MirroredRepeat)
        .value("ClampToEdge", TextureWrapping::ClampToEdge)
        .value("ClampToBorder", TextureWrapping::ClampToBorder);

    py::class_<Texture2D>(m, "Texture2D")
        .def(py::init([=](const char* image) {
                 stbi_set_flip_vertically_on_load(true);
                 int w, h, n;
                 auto data = stbi_load(image, &w, &h, &n, 3);
                 if (!data)
                 {
                     throw std::runtime_error(
                         std::string("Could not load image \"") + image + '\"');
                 }
                 stbi_set_flip_vertically_on_load(false);
                 SourceImageDescriptor srcDesc;
                 srcDesc.data = data;
                 srcDesc.width = w;
                 srcDesc.height = h;
                 srcDesc.format = Format::RGB8UInt;
                 auto texture = CreateTexture2D({}, srcDesc);
                 stbi_image_free(data);
                 checkOpenGLError();
                 return texture;
             }),
             py::arg("image"))
        .def(
            "setWrappingX",
            [=](Texture2D& self, TextureWrapping wrap) {
                glTextureParameteri(self.id, GL_TEXTURE_WRAP_S, wrap);
                checkOpenGLError();
            },
            py::arg("wrappingX"))
        .def(
            "setWrappingY",
            [=](Texture2D& self, TextureWrapping wrap) {
                glTextureParameteri(self.id, GL_TEXTURE_WRAP_T, wrap);
                checkOpenGLError();
            },
            py::arg("wrappingY"))
        .def(
            "setMinFilter",
            [=](Texture2D& self, TextureFilter filter) {
                glTextureParameteri(self.id, GL_TEXTURE_MIN_FILTER, filter);
                checkOpenGLError();
            },
            py::arg("minFilter"))
        .def(
            "setMagFilter",
            [=](Texture2D& self, TextureFilter filter) {
                glTextureParameteri(self.id, GL_TEXTURE_MAG_FILTER, filter);
                checkOpenGLError();
            },
            py::arg("magFilter"))
        .def("generateMipmap", &Texture2D::generateMipmap)
        .def_readonly("id", &Texture2D::id,
                      py::doc("Return value for glCreateTexture()"));

    py::enum_<DrawMode>(m, "DrawMode")
        .value("Triangles", DrawMode::Triangles)
        .value("TriangleFan", DrawMode::TriangleFan)
        .value("TriangleStrip", DrawMode::TriangleStrip)
        .value("Lines", DrawMode::Lines)
        .value("Points", DrawMode::Points);
    py::class_<Pipeline>(m, "Pipeline")
        .def(py::init([](DrawMode pt, py::args shaders) {
                 Pipeline self;
                 self.id = glCreateProgram();
                 self.drawMode = pt;
                 for (auto shader : shaders)
                 {
                     glAttachShader(self.id, shader.cast<uint32_t>());
                 }
                 glLinkProgram(self.id);
                 if (self.hasReport())
                 {
                     throw std::runtime_error(self.getReport());
                 }
                 return self;
             }),
             py::arg("primitive_topology"),
             py::doc("Create graphics line with primitive topology (Triangles "
                     "or TriangleFan)"))
        .def("activate", &Pipeline::activate,
             py::doc("Activate graphics pipeline"))
        .def(
            "setVertexBuffer",
            [=](Pipeline& self, Buffer buffer) {
                self.setVertexBuffer(buffer);
                checkOpenGLError();
            },
            py::arg("buffer"))
        .def(
            "setIndexBuffer",
            [=](Pipeline& self, Buffer buffer) {
                self.setIndexBuffer(buffer);
                checkOpenGLError();
            },
            py::arg("buffer"))
        .def(
            "setTexture2D",
            [=](Pipeline& self, Texture2D tex, int texUnit) {
                glActiveTexture(GL_TEXTURE0 + texUnit);
                glBindTexture(GL_TEXTURE_2D, tex.id);
                checkOpenGLError();
            },
            py::arg("texture"), py::arg("textureUnit"))
        .def("draw", &Pipeline::draw, py::arg("first"), py::arg("count"))
        .def(
            "drawIndexed",
            [=](Pipeline& self, size_t offset, size_t count) {
                self.drawIndexed((void*)offset, count);
                checkOpenGLError();
            },
            py::arg("offset"), py::arg("count"))
        .def(
            "getUniformLocation",
            [=](Pipeline& self, const char* name) {
                auto result = self.getUniformLocation(name);
                if (result == -1)
                {
                    throw std::runtime_error(
                        "Invalid uniform name. Uniform may be unused.");
                }
            },
            py::arg("name"))
        .def("setUniformInt", [](Pipeline& self, const char* name,
                                 int v) { self.setUniform(name, v); })
        .def(
            "setUniformFloat",
            [](Pipeline& self, const char* name, float v) {
                self.setUniform(name, v);
            },
            py::arg("name"), py::arg("value"))
        .def(
            "setUniformFloat",
            [](Pipeline& self, int id, float v) { self.setUniform(id, v); },
            py::arg("location"), py::arg("value"))
        .def(
            "setUniformVec2",
            [](Pipeline& self, const char* name, float x, float y) {
                self.setUniform(name, x, y);
            },
            py::arg("name"), py::arg("x"), py::arg("y"))
        .def(
            "setUniformVec2",
            [](Pipeline& self, int id, float x, float y) {
                self.setUniform(id, x, y);
            },
            py::arg("location"), py::arg("x"), py::arg("y"))
        .def(
            "setUniformVec3",
            [](Pipeline& self, const char* name, float x, float y, float z) {
                self.setUniform(name, x, y, z);
            },
            py::arg("name"), py::arg("x"), py::arg("y"), py::arg("z"))
        .def(
            "setUniformVec3",
            [](Pipeline& self, int id, float x, float y, float z) {
                self.setUniform(id, x, y, z);
            },
            py::arg("location"), py::arg("x"), py::arg("y"), py::arg("z"))
        .def(
            "setUniformVec4",
            [](Pipeline& self, const char* name, float x, float y, float z,
               float w) { self.setUniform(name, x, y, z, w); },
            py::arg("name"), py::arg("x"), py::arg("y"), py::arg("z"),
            py::arg("w"))
        .def(
            "setUniformVec4",
            [](Pipeline& self, int id, float x, float y, float z, float w) {
                self.setUniform(id, x, y, z, w);
            },
            py::arg("location"), py::arg("x"), py::arg("y"), py::arg("z"),
            py::arg("w"))
        .def("setUniform",
             [](Pipeline& self, const char* name, const py::args& args) {
                 auto v = args.cast<std::vector<float>>();
                 switch (v.size())
                 {
                     case 1: self.setUniform(name, v[0]); break;
                     case 2: self.setUniform(name, v[0], v[1]); break;
                     case 3: self.setUniform(name, v[0], v[1], v[2]); break;
                     case 4:
                         self.setUniform(name, v[0], v[1], v[2], v[3]);
                         break;
                 }
             })
        .def("setUniform",
             [](Pipeline& self, int id, const py::args& args) {
                 auto v = args.cast<std::vector<float>>();
                 switch (v.size())
                 {
                     case 1: self.setUniform(id, v[0]); break;
                     case 2: self.setUniform(id, v[0], v[1]); break;
                     case 3: self.setUniform(id, v[0], v[1], v[2]); break;
                     case 4: self.setUniform(id, v[0], v[1], v[2], v[3]); break;
                 }
             })
        .def("setTestVec3", [](Pipeline& self, int id, const py::object& obj) {
            py::print("%x", (size_t)obj.ptr() + 24);
            glProgramUniform3fv(self.id, id, 1,
                                (float*)(size_t(obj.ptr()) + 24));
        });

    auto u = py::class_<Utils>(m, "Utils");
    u.def_static(
         "vsFromFile",
         [](const char* filename) {
             ShaderDescriptor desc{VertexShader, SourceFile, filename};
             auto shader = CreateShader(desc);
             if (shader.hasReport())
             {
                 throw std::runtime_error(shader.getReport());
             }
             return shader.id;
         },
         py::arg("filename"), py::doc("Create vertex shader from file"))
        .def_static(
            "fsFromFile",
            [](const char* filename) {
                ShaderDescriptor desc{FragmentShader, SourceFile, filename};
                auto shader = CreateShader(desc);
                if (shader.hasReport())
                {
                    throw std::runtime_error(shader.getReport());
                }
                return shader.id;
            },
            py::arg("filename"), py::doc("Create fragment shader from file"))
        .def_static(
            "loadPipeline",
            [](const char* vsFile, const char* fsFile) {
                ShaderDescriptor desc{VertexShader, SourceFile, vsFile};
                auto vs = CreateShader(desc);
                if (vs.hasReport())
                {
                    throw std::runtime_error(vs.getReport());
                }
                desc.type = FragmentShader;
                desc.source = fsFile;
                auto fs = CreateShader(desc);
                if (fs.hasReport())
                {
                    throw std::runtime_error(fs.getReport());
                }
                PipelineDescriptor pipelineDesc;
                pipelineDesc.vertexShader = vs;
                pipelineDesc.fragmentShader = fs;
                pipelineDesc.drawMode = Triangles;
                auto pipeline = CreatePipeline(pipelineDesc);
                if (pipeline.hasReport())
                {
                    throw std::runtime_error(pipeline.getReport());
                }
                return pipeline;
            },
            py::arg("vsFile"), py::arg("fsFile"),
            py::doc("Load shaders and create pipeline"))
        .def_static(
            "createIndexBuffer",
            [](const py::object& list) {
                auto indices = list.cast<std::vector<int32_t>>();
                BufferDescriptor desc;
                desc.size = indices.size() * sizeof(uint32_t);
                desc.bindFlags = BindFlags::IndexBuffer;
                auto buffer = CreateBuffer(desc, indices.data());
                return buffer;
            },
            py::arg("indices"));
}